package de.sendorian.app.forumArchive;


import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;
import org.hamcrest.Matcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import de.sendorian.app.forumArchive.domain.Forum;
import de.sendorian.app.forumArchive.domain.Topic;

/**
 * Compresses PNG images with PNGout to save space with lossless optimizations.
 *
 * @author sendorian
 *
 */
@Component
public class Compressor {

	private static Logger LOG = Logger.getLogger(Compressor.class);

	private static final String PNGOUT = new File("bin/pngout").getAbsolutePath();

	private Matcher<Topic> topicMatcher;
	
	private Collector collector;
	
	private static final String os = System.getProperty("os.name");
	
	@Value("#{config.topicMatcher}")
	public void setTopicMatcher(Matcher<Topic> topicMatcher) {
		this.topicMatcher = topicMatcher;
	}

	@Autowired
	public void setCollector(Collector collector) {
		this.collector = collector;
	}


	/**
	 * Compresses all images in the topic folders that matches the filter. This is a workaround
	 * because it is not possible to get informations for the images which were contained in an
	 * archive. So simply all images in that folder will be compressed.
	 *
	 */
	public void compressImages(List<Forum> forums) {
		Set<String> allImages = collector.collectImagesInForumDirs(forums, topicMatcher);
		LOG.info("All images collected. Compressing " + allImages.size() + " images");
		compressImages(allImages);
	}


	public void compressImagesInFolders(Set<String> dirs) {
		Set<String> allFiles = collector.collectImagesInFolders(dirs);
		LOG.info("All images collected. Compressing " + allFiles.size() + " images");
		compressImages(allFiles);
	}


	/**
	 * Compresses all the images in the Set. (The Images have to have a fully qualified path!)
	 *
	 */
	public void compressImages(Set<String> images) {
		if (!isOperatingSystemSupported()) {
			LOG.warn("Compression is not supported on this operating system. Skipping...");
			return;
		}
		
		int processors = Runtime.getRuntime().availableProcessors();
		LOG.info("Using " + processors + " threads to compress images.");
		ExecutorService threadPool = Executors.newFixedThreadPool(processors);

		int processed = 1;
		for (String image1 : images) {
			Runnable compressionTask = createCompressionTask(image1,
					processed, images.size());
			threadPool.execute(compressionTask);
			processed++;
		}

		waitToComplete(threadPool);
	}

	
	/**
	 * Creates a Runnable that compresses the images.
	 */
	protected Runnable createCompressionTask(String image1,
			int processed, int totalCount) {
		return new CompressionTask(image1, processed, totalCount);
	}


	/**
	 * Waits until all tasks have been executed.
	 */
	private void waitToComplete(ExecutorService threadPool) {
		LOG.info("All compression tasks have been initiated. Waiting for tasks to complete.");
		threadPool.shutdown();
		try {
			threadPool.awaitTermination(120 * 60, TimeUnit.SECONDS);
			LOG.info("All compression tasks have been completed.");
		} catch (InterruptedException e) {
			LOG.error("Compressor was interrupted!", e);
		}
	}
	
	
	/**
	 * Checks if the current operating system is supported, i.e. if
	 * a pngout implementation is present.
	 */
	private boolean isOperatingSystemSupported() {
		return os.equals("Mac OS X");
	}

	
	private static final class CompressionTask implements Runnable {

		String fileName;
		int processed;
		int totalCount;

		CompressionTask(String fileName, int processed, int totalCount) {
			this.fileName = fileName;
			this.processed = processed;
			this.totalCount = totalCount;
		}


		public void run() {
			Process proc = null;
			try {
				long begin = System.currentTimeMillis();
				proc = startProcess(fileName);
				endProcess(proc, fileName, begin);
				LOG.info("Processed image " + processed + " of " + totalCount);
			} catch (Exception e) {
				LOG.error("Could not execute pngout!", e);
			} finally {
				close(proc);
			}
		}


		private Process startProcess(String fileName) throws IOException {
			LOG.debug("Compressing \"" + fileName + "\"");
			return new ProcessBuilder(PNGOUT, fileName).start();
		}


		private void endProcess(Process proc, String fileName, long begin)
		    throws InterruptedException {
			proc.waitFor();
			if (proc.exitValue() != 0) {
				LOG.error("Error executing PNGOUT on " + fileName);
			} else {
				long time = System.currentTimeMillis() - begin;
				BigDecimal timeInSeconds = new BigDecimal(time / 1000).setScale(1,
				    BigDecimal.ROUND_HALF_UP);
				LOG.debug("Finished Compressing \"" + fileName + "\" in "
				    + timeInSeconds + " [s]");
			}
		}


		private void close(Closeable c) {
			if (c != null) {
				try {
					c.close();
				} catch (IOException e) {
					// ignored
				}
			}
		}


		private void close(Process proc) {
			if (proc != null) {
				close(proc.getOutputStream());
				close(proc.getInputStream());
				close(proc.getErrorStream());
				proc.destroy();
			}
		}
	}
}
